<head><meta charset="UTF-8"></head><h1 class="heading">redo日志（下）</h1>
<p>标签： MySQL是怎样运行的</p>
<hr>
<h2 class="heading">redo日志文件</h2>
<h3 class="heading">redo日志刷盘时机</h3>
<p>我们前边说<code>mtr</code>运行过程中产生的一组<code>redo</code>日志在<code>mtr</code>结束时会被复制到<code>log buffer</code>中，可是这些日志总在内存里呆着也不是个办法，在一些情况下它们会被刷新到磁盘里，比如：</p>
<ul>
<li>
<p><code>log buffer</code>空间不足时</p>
<p><code>log buffer</code>的大小是有限的（通过系统变量<code>innodb_log_buffer_size</code>指定），如果不停的往这个有限大小的<code>log buffer</code>里塞入日志，很快它就会被填满。设计<code>InnoDB</code>的大叔认为如果当前写入<code>log buffer</code>的<code>redo</code>日志量已经占满了<code>log buffer</code>总容量的大约一半左右，就需要把这些日志刷新到磁盘上。</p>
</li>
<li>
<p>事务提交时</p>
<p>我们前边说过之所以使用<code>redo</code>日志主要是因为它占用的空间少，还是顺序写，在事务提交时可以不把修改过的<code>Buffer Pool</code>页面刷新到磁盘，但是为了保证持久性，必须要把修改这些页面对应的<code>redo</code>日志刷新到磁盘。</p>
</li>
<li>
<p>后台线程不停的刷刷刷</p>
<p>后台有一个线程，大约每秒都会刷新一次<code>log buffer</code>中的<code>redo</code>日志到磁盘。</p>
</li>
<li>
<p>正常关闭服务器时</p>
</li>
<li>
<p>做所谓的<code>checkpoint</code>时（我们现在没介绍过<code>checkpoint</code>的概念，稍后会仔细唠叨，稍安勿躁）</p>
</li>
<li>
<p>其他的一些情况...</p>
</li>
</ul>
<h3 class="heading">redo日志文件组</h3>
<p><code>MySQL</code>的数据目录（使用<code>SHOW VARIABLES LIKE 'datadir'</code>查看）下默认有两个名为<code>ib_logfile0</code>和<code>ib_logfile1</code>的文件，<code>log buffer</code>中的日志默认情况下就是刷新到这两个磁盘文件中。如果我们对默认的<code>redo</code>日志文件不满意，可以通过下边几个启动参数来调节：</p>
<ul>
<li>
<p><code>innodb_log_group_home_dir</code></p>
<p>该参数指定了<code>redo</code>日志文件所在的目录，默认值就是当前的数据目录。</p>
</li>
<li>
<p><code>innodb_log_file_size</code></p>
<p>该参数指定了每个<code>redo</code>日志文件的大小，在<code>MySQL 5.7.21</code>这个版本中的默认值为<code>48MB</code>，</p>
</li>
<li>
<p><code>innodb_log_files_in_group</code></p>
<p>该参数指定<code>redo</code>日志文件的个数，默认值为2，最大值为100。</p>
</li>
</ul>
<p>从上边的描述中可以看到，磁盘上的<code>redo</code>日志文件不只一个，而是以一个<code>日志文件组</code>的形式出现的。这些文件以<code>ib_logfile[数字]</code>（<code>数字</code>可以是<code>0</code>、<code>1</code>、<code>2</code>...）的形式进行命名。在将<code>redo</code>日志写入<code>日志文件组</code>时，是从<code>ib_logfile0</code>开始写，如果<code>ib_logfile0</code>写满了，就接着<code>ib_logfile1</code>写，同理，<code>ib_logfile1</code>写满了就去写<code>ib_logfile2</code>，依此类推。如果写到最后一个文件该咋办？那就重新转到<code>ib_logfile0</code>继续写，所以整个过程如下图所示：</p>
<p></p><figure><img alt="image_1d4mu4s6f7491l7l1jcc6pc1rbk16.png-49.7kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899033f3b35d?w=927&amp;h=328&amp;f=png&amp;s=50859"><figcaption></figcaption></figure><p></p>
<p>总共的<code>redo</code>日志文件大小其实就是：<code>innodb_log_file_size × innodb_log_files_in_group</code>。</p>
<blockquote class="warning"><p>小贴士：如果采用循环使用的方式向redo日志文件组里写数据的话，那岂不是要追尾，也就是后写入的redo日志覆盖掉前边写的redo日志？当然可能了！所以设计InnoDB的大叔提出了checkpoint的概念，稍后我们重点唠叨～
</p></blockquote><h3 class="heading">redo日志文件格式</h3>
<p>我们前边说过<code>log buffer</code>本质上是一片连续的内存空间，被划分成了若干个<code>512</code>字节大小的<code>block</code>。<span style="color:red">将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入日志文件中</span>，所以<code>redo</code>日志文件其实也是由若干个<code>512</code>字节大小的block组成。</p>
<p><code>redo</code>日志文件组中的每个文件大小都一样，格式也一样，都是由两部分组成：</p>
<ul>
<li>
<p>前2048个字节，也就是前4个block是用来存储一些管理信息的。</p>
</li>
<li>
<p>从第2048字节往后是用来存储<code>log buffer</code>中的block镜像的。</p>
</li>
</ul>
<p>所以我们前边所说的<code>循环</code>使用redo日志文件，其实是从每个日志文件的第2048个字节开始算，画个示意图就是这样：</p>
<p></p><figure><img alt="image_1d4njgt351je21kitk7u1gbioa46j.png-64.9kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899033d57b7a?w=965&amp;h=517&amp;f=png&amp;s=66461"><figcaption></figcaption></figure><p></p>
<p>普通block的格式我们在唠叨<code>log buffer</code>的时候都说过了，就是<code>log block header</code>、<code>log block body</code>、<code>log block trialer</code>这三个部分，就不重复介绍了。这里需要介绍一下每个<code>redo</code>日志文件前2048个字节，也就是前4个特殊block的格式都是干嘛的，废话少说，先看图：</p>
<p></p><figure><img alt="image_1d4n63euu1t3u1ten1tgicecsar4c.png-51.1kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899033e1cb89?w=1012&amp;h=311&amp;f=png&amp;s=52334"><figcaption></figcaption></figure><br>
从图中可以看出来，这4个block分别是：<p></p>
<ul>
<li>
<p><code>log file header</code>：描述该<code>redo</code>日志文件的一些整体属性，看一下它的结构：</p>
<p></p><figure><img alt="image_1d4nfhoa914vbne4kao7cstr95m.png-65.5kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899065200011?w=690&amp;h=534&amp;f=png&amp;s=67070"><figcaption></figcaption></figure><p></p>
<p>各个属性的具体释义如下：</p>
<table>
<thead>
<tr>
<th style="text-align:center">属性名</th>
<th style="text-align:center">长度（单位：字节）</th>
<th style="text-align:left">描述</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center"><code>LOG_HEADER_FORMAT</code></td>
<td style="text-align:center"><code>4</code></td>
<td style="text-align:left"><code>redo</code>日志的版本，在<code>MySQL 5.7.21</code>中该值永远为1</td>
</tr>
<tr>
<td style="text-align:center"><code>LOG_HEADER_PAD1</code></td>
<td style="text-align:center"><code>4</code></td>
<td style="text-align:left">做字节填充用的，没什么实际意义，忽略～</td>
</tr>
<tr>
<td style="text-align:center"><code>LOG_HEADER_START_LSN</code></td>
<td style="text-align:center"><code>8</code></td>
<td style="text-align:left">标记本<code>redo</code>日志文件开始的LSN值，也就是文件偏移量为2048字节初对应的LSN值（关于什么是LSN我们稍后再看哈，看不懂的先忽略）。</td>
</tr>
<tr>
<td style="text-align:center"><code>LOG_HEADER_CREATOR</code></td>
<td style="text-align:center"><code>32</code></td>
<td style="text-align:left">一个字符串，标记本<code>redo</code>日志文件的创建者是谁。正常运行时该值为<code>MySQL</code>的版本号，比如：<code>"MySQL 5.7.21"</code>，使用<code>mysqlbackup</code>命令创建的<code>redo</code>日志文件的该值为<code>"ibbackup"</code>和创建时间。</td>
</tr>
<tr>
<td style="text-align:center"><code>LOG_BLOCK_CHECKSUM</code></td>
<td style="text-align:center"><code>4</code></td>
<td style="text-align:left">本block的校验值，所有block都有，我们不关心</td>
</tr>
</tbody>
</table>
<blockquote class="warning"><p>小贴士：

设计InnoDB的大叔对redo日志的block格式做了很多次修改，如果你阅读的其他书籍中发现上述的属性和你阅读书籍中的属性有些出入，不要慌，正常现象，忘记以前的版本吧。另外，LSN值我们后边才会介绍，现在千万别纠结LSN是个啥。
</p></blockquote></li>
<li>
<p><code>checkpoint1</code>：记录关于<code>checkpoint</code>的一些属性，看一下它的结构：</p>
<p></p><figure><img alt="image_1d4njq08pd2a5j9pc01qcn2ps7g.png-60.1kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899037defb21?w=564&amp;h=487&amp;f=png&amp;s=61493"><figcaption></figcaption></figure><p></p>
<p>各个属性的具体释义如下：</p>
<table>
<thead>
<tr>
<th style="text-align:center">属性名</th>
<th style="text-align:center">长度（单位：字节）</th>
<th style="text-align:left">描述</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center"><code>LOG_CHECKPOINT_NO</code></td>
<td style="text-align:center"><code>8</code></td>
<td style="text-align:left">服务器做<code>checkpoint</code>的编号，每做一次<code>checkpoint</code>，该值就加1。</td>
</tr>
<tr>
<td style="text-align:center"><code>LOG_CHECKPOINT_LSN</code></td>
<td style="text-align:center"><code>8</code></td>
<td style="text-align:left">服务器做<code>checkpoint</code>结束时对应的<code>LSN</code>值，系统奔溃恢复时将从该值开始。</td>
</tr>
<tr>
<td style="text-align:center"><code>LOG_CHECKPOINT_OFFSET</code></td>
<td style="text-align:center"><code>8</code></td>
<td style="text-align:left">上个属性中的<code>LSN</code>值在<code>redo</code>日志文件组中的偏移量</td>
</tr>
<tr>
<td style="text-align:center"><code>LOG_CHECKPOINT_LOG_BUF_SIZE</code></td>
<td style="text-align:center"><code>8</code></td>
<td style="text-align:left">服务器在做<code>checkpoint</code>操作时对应的<code>log buffer</code>的大小</td>
</tr>
<tr>
<td style="text-align:center"><code>LOG_BLOCK_CHECKSUM</code></td>
<td style="text-align:center"><code>4</code></td>
<td style="text-align:left">本block的校验值，所有block都有，我们不关心</td>
</tr>
</tbody>
</table>
<blockquote class="warning"><p>小贴士：

现在看不懂上边这些关于checkpoint和LSN的属性的释义是很正常的，我就是想让大家对上边这些属性混个脸熟，后边我们后详细唠叨的。
</p></blockquote></li>
<li>
<p>第三个block未使用，忽略～</p>
</li>
<li>
<p><code>checkpoint2</code>：结构和<code>checkpoint1</code>一样。</p>
</li>
</ul>
<h2 class="heading">Log Sequeue Number</h2>
<p>自系统开始运行，就不断的在修改页面，也就意味着会不断的生成<code>redo</code>日志。<code>redo</code>日志的量在不断的递增，就像人的年龄一样，自打出生起就不断递增，永远不可能缩减了。设计<code>InnoDB</code>的大叔为记录已经写入的<code>redo</code>日志量，设计了一个称之为<code>Log Sequeue Number</code>的全局变量，翻译过来就是：<code>日志序列号</code>，简称<code>lsn</code>。不过不像人一出生的年龄是<code>0</code>岁，设计<code>InnoDB</code>的大叔<span style="color:red">规定</span>初始的<code>lsn</code>值为<code>8704</code>（也就是一条<code>redo</code>日志也没写入时，<code>lsn</code>的值为<code>8704</code>）。</p>
<p>我们知道在向<code>log buffer</code>中写入<code>redo</code>日志时不是一条一条写入的，而是以一个<code>mtr</code>生成的一组<code>redo</code>日志为单位进行写入的。而且实际上是把日志内容写在了<code>log block body</code>处。但是在统计<code>lsn</code>的增长量时，是按照实际写入的日志量加上占用的<code>log block header</code>和<code>log block trailer</code>来计算的。我们来看一个例子：</p>
<ul>
<li>
<p>系统第一次启动后初始化<code>log buffer</code>时，<code>buf_free</code>（就是标记下一条<code>redo</code>日志应该写入到<code>log buffer</code>的位置的变量）就会指向第一个<code>block</code>的偏移量为12字节（<code>log block header</code>的大小）的地方，那么<code>lsn</code>值也会跟着增加12：</p>
<p></p><figure><img alt="image_1d4v2r59mr10jdl1vs4fk61huv79.png-50.9kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899065393c60?w=844&amp;h=298&amp;f=png&amp;s=52083"><figcaption></figcaption></figure><p></p>
</li>
<li>
<p>如果某个<code>mtr</code>产生的一组<code>redo</code>日志占用的存储空间比较小，也就是待插入的block剩余空闲空间能容纳这个<code>mtr</code>提交的日志时，<code>lsn</code>增长的量就是该<code>mtr</code>生成的<code>redo</code>日志占用的字节数，就像这样：</p>
<p></p><figure><img alt="image_1d4v57vgl1obr1kfcfuunp44bo2t.png-54kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899036dc68a0?w=881&amp;h=318&amp;f=png&amp;s=55320"><figcaption></figcaption></figure><p></p>
<p>我们假设上图中<code>mtr_1</code>产生的<code>redo</code>日志量为200字节，那么<code>lsn</code>就要在<code>8716</code>的基础上增加<code>200</code>，变为<code>8916</code>。</p>
</li>
<li>
<p>如果某个<code>mtr</code>产生的一组<code>redo</code>日志占用的存储空间比较大，也就是待插入的block剩余空闲空间不足以容纳这个<code>mtr</code>提交的日志时，<code>lsn</code>增长的量就是该<code>mtr</code>生成的<code>redo</code>日志占用的字节数加上额外占用的<code>log block header</code>和<code>log block trailer</code>的字节数，就像这样：</p>
<p></p><figure><img alt="image_1d4v37u011jhc1rpa1fpi5a82ca9.png-99.3kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899037f19b86?w=957&amp;h=523&amp;f=png&amp;s=101664"><figcaption></figcaption></figure><p></p>
<p>我们假设上图中<code>mtr_2</code>产生的<code>redo</code>日志量为1000字节，为了将<code>mtr_2</code>产生的<code>redo</code>日志写入<code>log buffer</code>，我们不得不额外多分配两个block，所以<code>lsn</code>的值需要在<code>8916</code>的基础上增加<code>1000 + 12×2 + 4 × 2 = 1032</code>。</p>
</li>
</ul>
<blockquote class="warning"><p>小贴士：

为什么初始的lsn值为8704呢？我也不太清楚，人家就这么规定的。其实你也可以规定你一生下来算1岁，只要保证随着时间的流逝，你的年龄不断增长就好了。
</p></blockquote><p>从上边的描述中可以看出来，<span style="color:red">每一组由mtr生成的redo日志都有一个唯一的LSN值与其对应，LSN值越小，说明redo日志产生的越早</span>。</p>
<h3 class="heading">flushed_to_disk_lsn</h3>
<p><code>redo</code>日志是首先写到<code>log buffer</code>中，之后才会被刷新到磁盘上的<code>redo</code>日志文件。所以设计<code>InnoDB</code>的大叔提出了一个称之为<code>buf_next_to_write</code>的全局变量，标记当前<code>log buffer</code>中已经有哪些日志被刷新到磁盘中了。画个图表示就是这样：</p>
<p></p><figure><img alt="image_1d4q3upvq17n8cargmibugve29.png-84.3kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899065cd1697?w=821&amp;h=410&amp;f=png&amp;s=86278"><figcaption></figcaption></figure><p></p>
<p>我们前边说<code>lsn</code>是表示当前系统中写入的<code>redo</code>日志量，这包括了写到<code>log buffer</code>而没有刷新到磁盘的日志，相应的，设计<code>InnoDB</code>的大叔提出了一个表示刷新到磁盘中的<code>redo</code>日志量的全局变量，称之为<code>flushed_to_disk_lsn</code>。系统第一次启动时，该变量的值和初始的<code>lsn</code>值是相同的，都是<code>8704</code>。随着系统的运行，<code>redo</code>日志被不断写入<code>log buffer</code>，但是并不会立即刷新到磁盘，<code>lsn</code>的值就和<code>flushed_to_disk_lsn</code>的值拉开了差距。我们演示一下：</p>
<ul>
<li>
<p>系统第一次启动后，向<code>log buffer</code>中写入了<code>mtr_1</code>、<code>mtr_2</code>、<code>mtr_3</code>这三个<code>mtr</code>产生的<code>redo</code>日志，假设这三个<code>mtr</code>开始和结束时对应的lsn值分别是：</p>
<ul>
<li><code>mtr_1</code>：8716 ～ 8916</li>
<li><code>mtr_2</code>：8916 ～ 9948</li>
<li><code>mtr_3</code>：9948 ～ 10000</li>
</ul>
<p>此时的<code>lsn</code>已经增长到了10000，但是由于没有刷新操作，所以此时<code>flushed_to_disk_lsn</code>的值仍为<code>8704</code>，如图：</p>
<p></p><figure><img alt="image_1d4v3ubbacgm13171s481trb6kj1m.png-88.5kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899065f16c34?w=1062&amp;h=540&amp;f=png&amp;s=90575"><figcaption></figcaption></figure><p></p>
</li>
<li>
<p>随后进行将<code>log buffer</code>中的block刷新到<code>redo</code>日志文件的操作，假设将<code>mtr_1</code>和<code>mtr_2</code>的日志刷新到磁盘，那么<code>flushed_to_disk_lsn</code>就应该增长<code>mtr_1</code>和<code>mtr_2</code>写入的日志量，所以<code>flushed_to_disk_lsn</code>的值增长到了<code>9948</code>，如图：</p>
<p></p><figure><img alt="image_1d4v40upc1tnt1dpe1l14u2ar4n23.png-100.2kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899065ece690?w=1047&amp;h=553&amp;f=png&amp;s=102599"><figcaption></figcaption></figure><p></p>
</li>
</ul>
<p>综上所述，当有新的<code>redo</code>日志写入到<code>log buffer</code>时，首先<code>lsn</code>的值会增长，但<code>flushed_to_disk_lsn</code>不变，随后随着不断有<code>log buffer</code>中的日志被刷新到磁盘上，<code>flushed_to_disk_lsn</code>的值也跟着增长。<span style="color:red">如果两者的值相同时，说明log buffer中的所有redo日志都已经刷新到磁盘中了</span>。</p>
<blockquote class="warning"><p>小贴士：

应用程序向磁盘写入文件时其实是先写到操作系统的缓冲区中去，如果某个写入操作要等到操作系统确认已经写到磁盘时才返回，那需要调用一下操作系统提供的fsync函数。其实只有当系统执行了fsync函数后，flushed_to_disk_lsn的值才会跟着增长，当仅仅把log buffer中的日志写入到操作系统缓冲区却没有显式的刷新到磁盘时，另外的一个称之为write_lsn的值跟着增长。不过为了大家理解上的方便，我们在讲述时把flushed_to_disk_lsn和write_lsn的概念混淆了起来。
</p></blockquote><h3 class="heading">lsn值和redo日志文件偏移量的对应关系</h3>
<p>因为<code>lsn</code>的值是代表系统写入的<code>redo</code>日志量的一个总和，一个<code>mtr</code>中产生多少日志，<code>lsn</code>的值就增加多少（当然有时候要加上<code>log block header</code>和<code>log block trailer</code>的大小），这样<code>mtr</code>产生的日志写到磁盘中时，很容易计算某一个<code>lsn</code>值在<code>redo</code>日志文件组中的偏移量，如图：</p>
<p></p><figure><img alt="image_1d4v5sdrj1p1jrhmnfrq4pa073n.png-49.3kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b8990864b2982?w=1043&amp;h=304&amp;f=png&amp;s=50514"><figcaption></figcaption></figure><p></p>
<p>初始时的<code>LSN</code>值是<code>8704</code>，对应文件偏移量<code>2048</code>，之后每个<code>mtr</code>向磁盘中写入多少字节日志，<code>lsn</code>的值就增长多少。</p>
<h3 class="heading">flush链表中的LSN</h3>
<p>我们知道一个<code>mtr</code>代表一次对底层页面的原子访问，在访问过程中可能会产生一组不可分割的<code>redo</code>日志，在<code>mtr</code>结束时，会把这一组<code>redo</code>日志写入到<code>log buffer</code>中。除此之外，在<code>mtr</code>结束时还有一件非常重要的事情要做，就是<span style="color:red">把在mtr执行过程中可能修改过的页面加入到Buffer Pool的flush链表</span>。为了防止大家早已忘记<code>flush链表</code>是个啥，我们再看一下图：</p>
<p></p><figure><img alt="image_1d4uln1ejrt4cerr6h1tc41uok3k.png-227kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899094bcc632?w=1010&amp;h=591&amp;f=png&amp;s=232466"><figcaption></figcaption></figure><p></p>
<p>当第一次修改某个缓存在<code>Buffer Pool</code>中的页面时，就会把这个页面对应的控制块插入到<code>flush链表</code>的头部，之后再修改该页面时由于它已经在<code>flush</code>链表中了，就不再次插入了。也就是说<span style="color:red">flush链表中的脏页是按照页面的第一次修改时间从大到小进行排序的</span>。在这个过程中会在缓存页对应的控制块中记录两个关于页面何时修改的属性：</p>
<ul>
<li>
<p><code>oldest_modification</code>：如果某个页面被加载到<code>Buffer Pool</code>后进行第一次修改，那么就将修改该页面的<code>mtr</code>开始时对应的<code>lsn</code>值写入这个属性。</p>
</li>
<li>
<p><code>newest_modification</code>：每修改一次页面，都会将修改该页面的<code>mtr</code>结束时对应的<code>lsn</code>值写入这个属性。也就是说该属性表示页面最近一次修改后对应的系统<code>lsn</code>值。</p>
</li>
</ul>
<p>我们接着上边唠叨<code>flushed_to_disk_lsn</code>的例子看一下：</p>
<ul>
<li>
<p>假设<code>mtr_1</code>执行过程中修改了<code>页a</code>，那么在<code>mtr_1</code>执行结束时，就会将<code>页a</code>对应的控制块加入到<code>flush链表</code>的头部。并且将<code>mtr_1</code>开始时对应的<code>lsn</code>，也就是<code>8716</code>写入<code>页a</code>对应的控制块的<code>oldest_modification</code>属性中，把<code>mtr_1</code>结束时对应的<code>lsn</code>，也就是8916写入<code>页a</code>对应的控制块的<code>newest_modification</code>属性中。画个图表示一下（为了让图片美观一些，我们把<code>oldest_modification</code>缩写成了<code>o_m</code>，把<code>newest_modification</code>缩写成了<code>n_m</code>）：</p>
<p></p><figure><img alt="image_1d4v63pct1v9o14l3812gnj11de44.png-31.8kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899088d4f875?w=452&amp;h=302&amp;f=png&amp;s=32582"><figcaption></figcaption></figure><p></p>
</li>
<li>
<p>接着假设<code>mtr_2</code>执行过程中又修改了<code>页b</code>和<code>页c</code>两个页面，那么在<code>mtr_2</code>执行结束时，就会将<code>页b</code>和<code>页c</code>对应的控制块都加入到<code>flush链表</code>的头部。并且将<code>mtr_2</code>开始时对应的<code>lsn</code>，也就是8916写入<code>页b</code>和<code>页c</code>对应的控制块的<code>oldest_modification</code>属性中，把<code>mtr_2</code>结束时对应的<code>lsn</code>，也就是9948写入<code>页b</code>和<code>页c</code>对应的控制块的<code>newest_modification</code>属性中。画个图表示一下：</p>
<p></p><figure><img alt="image_1d4v64vte14tq1oc911s1v8gnn51.png-59.4kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b899094ab6caf?w=918&amp;h=322&amp;f=png&amp;s=60786"><figcaption></figcaption></figure><p></p>
<p>从图中可以看出来，每次新插入到<code>flush链表</code>中的节点都是被放在了头部，也就是说<code>flush链表</code>中前边的脏页修改的时间比较晚，后边的脏页修改时间比较早。</p>
</li>
<li>
<p>接着假设<code>mtr_3</code>执行过程中修改了<code>页b</code>和<code>页d</code>，不过<code>页b</code>之前已经被修改过了，所以它对应的控制块已经被插入到了<code>flush</code>链表，所以在<code>mtr_3</code>执行结束时，只需要将<code>页d</code>对应的控制块都加入到<code>flush链表</code>的头部即可。所以需要将<code>mtr_3</code>开始时对应的<code>lsn</code>，也就是9948写入<code>页d</code>对应的控制块的<code>oldest_modification</code>属性中，把<code>mtr_3</code>结束时对应的<code>lsn</code>，也就是10000写入<code>页d</code>对应的控制块的<code>newest_modification</code>属性中。另外，由于<code>页b</code>在<code>mtr_3</code>执行过程中又发生了一次修改，所以需要更新<code>页b</code>对应的控制块中<code>newest_modification</code>的值为10000。画个图表示一下：</p>
<p></p><figure><img alt="image_1d4v68bhl1jb9r8m6vn1b157cn5e.png-110.8kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b89909693bfe9?w=1127&amp;h=484&amp;f=png&amp;s=113430"><figcaption></figcaption></figure><p></p>
</li>
</ul>
<p>总结一下上边说的，就是：<span style="color:red">flush链表中的脏页按照修改发生的时间顺序进行排序，也就是按照oldest_modification代表的LSN值进行排序，被多次更新的页面不会重复插入到flush链表中，但是会更新newest_modification属性的值</span>。</p>
<h2 class="heading">checkpoint</h2>
<p>有一个很不幸的事实就是我们的<code>redo</code>日志文件组容量是有限的，我们不得不选择循环使用<code>redo</code>日志文件组中的文件，但是这会造成最后写的<code>redo</code>日志与最开始写的<code>redo</code>日志<code>追尾</code>，这时应该想到：<span style="color:red">redo日志只是为了系统奔溃后恢复脏页用的，如果对应的脏页已经刷新到了磁盘，也就是说即使现在系统奔溃，那么在重启后也用不着使用redo日志恢复该页面了，所以该redo日志也就没有存在的必要了，那么它占用的磁盘空间就可以被后续的redo日志所重用</span>。也就是说：<span style="color:red">判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里</span>。我们看一下前边一直唠叨的那个例子：</p>
<p></p><figure><img alt="image_1d4v6epcasjm11u4l131nj41vgs68.png-112.1kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b8990a1ec0f87?w=793&amp;h=638&amp;f=png&amp;s=114802"><figcaption></figcaption></figure><p></p>
<p>如图，虽然<code>mtr_1</code>和<code>mtr_2</code>生成的<code>redo</code>日志都已经被写到了磁盘上，但是它们修改的脏页仍然留在<code>Buffer Pool</code>中，所以它们生成的<code>redo</code>日志在磁盘上的空间是不可以被覆盖的。之后随着系统的运行，如果<code>页a</code>被刷新到了磁盘，那么它对应的控制块就会从<code>flush链表</code>中移除，就像这样子：</p>
<p></p><figure><img alt="image_1d4v6h6kp7311ni21mkn1ejkm397i.png-99.3kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b8990ad89ad77?w=632&amp;h=627&amp;f=png&amp;s=101718"><figcaption></figcaption></figure><p></p>
<p>这样<code>mtr_1</code>生成的<code>redo</code>日志就没有用了，它们占用的磁盘空间就可以被覆盖掉了。设计<code>InnoDB</code>的大叔提出了一个全局变量<code>checkpoint_lsn</code>来代表当前系统中可以被覆盖的<code>redo</code>日志总量是多少，这个变量初始值也是<code>8704</code>。</p>
<p>比方说现在<code>页a</code>被刷新到了磁盘，<code>mtr_1</code>生成的<code>redo</code>日志就可以被覆盖了，所以我们可以进行一个增加<code>checkpoint_lsn</code>的操作，我们把这个过程称之为做一次<code>checkpoint</code>。做一次<code>checkpoint</code>其实可以分为两个步骤：</p>
<ul>
<li>
<p>步骤一：计算一下当前系统中可以被覆盖的<code>redo</code>日志对应的<code>lsn</code>值最大是多少。</p>
<p><code>redo</code>日志可以被覆盖，意味着它对应的脏页被刷到了磁盘，只要我们计算出当前系统中被最早修改的脏页对应的<code>oldest_modification</code>值，那<span style="color:red">凡是在系统lsn值小于该节点的oldest_modification值时产生的redo日志都是可以被覆盖掉的</span>，我们就把该脏页的<code>oldest_modification</code>赋值给<code>checkpoint_lsn</code>。</p>
<p>比方说当前系统中<code>页a</code>已经被刷新到磁盘，那么<code>flush链表</code>的尾节点就是<code>页c</code>，该节点就是当前系统中最早修改的脏页了，它的<code>oldest_modification</code>值为8916，我们就把8916赋值给<code>checkpoint_lsn</code>（也就是说在redo日志对应的lsn值小于8916时就可以被覆盖掉）。</p>
</li>
<li>
<p>步骤二：将<code>checkpoint_lsn</code>和对应的<code>redo</code>日志文件组偏移量以及此次<code>checkpint</code>的编号写到日志文件的管理信息（就是<code>checkpoint1</code>或者<code>checkpoint2</code>）中。</p>
<p>设计<code>InnoDB</code>的大叔维护了一个目前系统做了多少次<code>checkpoint</code>的变量<code>checkpoint_no</code>，每做一次<code>checkpoint</code>，该变量的值就加1。我们前边说过计算一个<code>lsn</code>值对应的<code>redo</code>日志文件组偏移量是很容易的，所以可以计算得到该<code>checkpoint_lsn</code>在<code>redo</code>日志文件组中对应的偏移量<code>checkpoint_offset</code>，然后把这三个值都写到<code>redo</code>日志文件组的管理信息中。</p>
<p>我们说过，每一个<code>redo</code>日志文件都有<code>2048</code>个字节的管理信息，但是<span style="color:red">上述关于checkpoint的信息只会被写到日志文件组的第一个日志文件的管理信息中</span>。不过我们是存储到<code>checkpoint1</code>中还是<code>checkpoint2</code>中呢？设计<code>InnoDB</code>的大叔规定，当<code>checkpoint_no</code>的值是偶数时，就写到<code>checkpoint1</code>中，是奇数时，就写到<code>checkpoint2</code>中。</p>
</li>
</ul>
<p>记录完<code>checkpoint</code>的信息之后，<code>redo</code>日志文件组中各个<code>lsn</code>值的关系就像这样：</p>
<p></p><figure><img alt="image_1d678eiie125j1flp1tc617jp1dvo9.png-68.1kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b8990aeb41002?w=956&amp;h=519&amp;f=png&amp;s=69737"><figcaption></figcaption></figure><p></p>
<h3 class="heading">批量从flush链表中刷出脏页</h3>
<p>我们在介绍<code>Buffer Pool</code>的时候说过，一般情况下都是后台的线程在对<code>LRU链表</code>和<code>flush链表</code>进行刷脏操作，这主要因为刷脏操作比较慢，不想影响用户线程处理请求。但是如果当前系统修改页面的操作十分频繁，这样就导致写日志操作十分频繁，系统<code>lsn</code>值增长过快。如果后台的刷脏操作不能将脏页刷出，那么系统无法及时做<code>checkpoint</code>，可能就需要用户线程同步的从<code>flush链表</code>中把那些最早修改的脏页（<code>oldest_modification</code>最小的脏页）刷新到磁盘，这样这些脏页对应的<code>redo</code>日志就没用了，然后就可以去做<code>checkpoint</code>了。</p>
<h3 class="heading">查看系统中的各种LSN值</h3>
<p>我们可以使用<code>SHOW ENGINE INNODB STATUS</code>命令查看当前<code>InnoDB</code>存储引擎中的各种<code>LSN</code>值的情况，比如：</p>
<pre><code class="hljs bash" lang="bash">mysql&gt; SHOW ENGINE INNODB STATUS\G

(...省略前边的许多状态)
LOG
---
Log sequence number 124476971
Log flushed up to   124099769
Pages flushed up to 124052503
Last checkpoint at  124052494
0 pending <span class="hljs-built_in">log</span> flushes, 0 pending chkp writes
24 <span class="hljs-built_in">log</span> i/o<span class="hljs-string">'s done, 2.00 log i/o'</span>s/second
----------------------
(...省略后边的许多状态)
</code></pre><p>其中：</p>
<ul>
<li>
<p><code>Log sequence number</code>：代表系统中的<code>lsn</code>值，也就是当前系统已经写入的<code>redo</code>日志量，包括写入<code>log buffer</code>中的日志。</p>
</li>
<li>
<p><code>Log flushed up to</code>：代表<code>flushed_to_disk_lsn</code>的值，也就是当前系统已经写入磁盘的<code>redo</code>日志量。</p>
</li>
<li>
<p><code>Pages flushed up to</code>：代表<code>flush链表</code>中被最早修改的那个页面对应的<code>oldest_modification</code>属性值。</p>
</li>
<li>
<p><code>Last checkpoint at</code>：当前系统的<code>checkpoint_lsn</code>值。</p>
</li>
</ul>
<h2 class="heading">innodb_flush_log_at_trx_commit的用法</h2>
<p>我们前边说为了保证事务的<code>持久性</code>，用户线程在事务提交时需要将该事务执行过程中产生的所有<code>redo</code>日志都刷新到磁盘上。这一条要求太狠了，会很明显的降低数据库性能。如果有的同学对事务的<code>持久性</code>要求不是那么强烈的话，可以选择修改一个称为<code>innodb_flush_log_at_trx_commit</code>的系统变量的值，该变量有3个可选的值：</p>
<ul>
<li>
<p><code>0</code>：当该系统变量值为0时，表示在事务提交时不立即向磁盘中同步<code>redo</code>日志，这个任务是交给后台线程做的。</p>
<p>这样很明显会加快请求处理速度，但是如果事务提交后服务器挂了，后台线程没有及时将<code>redo</code>日志刷新到磁盘，那么该事务对页面的修改会丢失。</p>
</li>
<li>
<p><code>1</code>：当该系统变量值为1时，表示在事务提交时需要将<code>redo</code>日志同步到磁盘，可以保证事务的<code>持久性</code>。<code>1</code>也是<code>innodb_flush_log_at_trx_commit</code>的默认值。</p>
</li>
<li>
<p><code>2</code>：当该系统变量值为2时，表示在事务提交时需要将<code>redo</code>日志写到操作系统的缓冲区中，但并不需要保证将日志真正的刷新到磁盘。</p>
<p>这种情况下如果数据库挂了，操作系统没挂的话，事务的<code>持久性</code>还是可以保证的，但是操作系统也挂了的话，那就不能保证<code>持久性</code>了。</p>
</li>
</ul>
<h2 class="heading">崩溃恢复</h2>
<p>在服务器不挂的情况下，<code>redo</code>日志简直就是个大累赘，不仅没用，反而让性能变得更差。但是万一，我说万一啊，万一数据库挂了，那<code>redo</code>日志可是个宝了，我们就可以在重启时根据<code>redo</code>日志中的记录就可以将页面恢复到系统奔溃前的状态。我们接下来大致看一下恢复过程是个啥样。</p>
<h3 class="heading">确定恢复的起点</h3>
<p>我们前边说过，<code>checkpoint_lsn</code>之前的<code>redo</code>日志都可以被覆盖，也就是说这些<code>redo</code>日志对应的脏页都已经被刷新到磁盘中了，既然它们已经被刷盘，我们就没必要恢复它们了。对于<code>checkpoint_lsn</code>之后的<code>redo</code>日志，它们对应的脏页可能没被刷盘，也可能被刷盘了，我们不能确定，所以需要从<code>checkpoint_lsn</code>开始读取<code>redo</code>日志来恢复页面。</p>
<p>当然，<code>redo</code>日志文件组的第一个文件的管理信息中有两个block都存储了<code>checkpoint_lsn</code>的信息，我们当然是要选取<span style="color:Red">最近发生的那次checkpoint的信息</span>。衡量<code>checkpoint</code>发生时间早晚的信息就是所谓的<code>checkpoint_no</code>，我们只要把<code>checkpoint1</code>和<code>checkpoint2</code>这两个block中的<code>checkpoint_no</code>值读出来比一下大小，哪个的<code>checkpoint_no</code>值更大，说明哪个block存储的就是最近的一次<code>checkpoint</code>信息。这样我们就能拿到最近发生的<code>checkpoint</code>对应的<code>checkpoint_lsn</code>值以及它在<code>redo</code>日志文件组中的偏移量<code>checkpoint_offset</code>。</p>
<h3 class="heading">确定恢复的终点</h3>
<p><code>redo</code>日志恢复的起点确定了，那终点是哪个呢？这个还得从block的结构说起。我们说在写<code>redo</code>日志的时候都是顺序写的，写满了一个block之后会再往下一个block中写：</p>
<p></p><figure><img alt="image_1d4viej35t9nvld8o3141s8pp.png-69.5kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b8990b6d085cd?w=851&amp;h=357&amp;f=png&amp;s=71131"><figcaption></figcaption></figure><p></p>
<p>普通block的<code>log block header</code>部分有一个称之为<code>LOG_BLOCK_HDR_DATA_LEN</code>的属性，该属性值记录了当前block里使用了多少字节的空间。对于被填满的block来说，该值永远为<code>512</code>。如果该属性的值不为<code>512</code>，那么就是它了，它就是此次奔溃恢复中需要扫描的最后一个block。</p>
<h3 class="heading">怎么恢复</h3>
<p>确定了需要扫描哪些<code>redo</code>日志进行奔溃恢复之后，接下来就是怎么进行恢复了。假设现在的<code>redo</code>日志文件中有5条<code>redo</code>日志，如图：</p>
<p></p><figure><img alt="image_1d4vjuf9l17og1papl3e16is1m9f16.png-59.9kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b8990b9780dc2?w=830&amp;h=288&amp;f=png&amp;s=61383"><figcaption></figcaption></figure><p></p>
<p>由于<code>redo 0</code>在<code>checkpoint_lsn</code>后边，恢复时可以不管它。我们现在可以按照<code>redo</code>日志的顺序依次扫描<code>checkpoint_lsn</code>之后的各条redo日志，按照日志中记载的内容将对应的页面恢复出来。这样没什么问题，不过设计<code>InnoDB</code>的大叔还是想了一些办法加快这个恢复的过程：</p>
<ul>
<li>
<p>使用哈希表</p>
<p>根据<code>redo</code>日志的<code>space ID</code>和<code>page number</code>属性计算出散列值，把<code>space ID</code>和<code>page number</code>相同的<code>redo</code>日志放到哈希表的同一个槽里，如果有多个<code>space ID</code>和<code>page number</code>都相同的<code>redo</code>日志，那么它们之间使用链表连接起来，按照生成的先后顺序链接起来的，如图所示：</p>
<p></p><figure><img alt="image_1d50lj9da176rojd12ja1lodognc.png-156.4kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b8990d0161bfc?w=1112&amp;h=634&amp;f=png&amp;s=160128"><figcaption></figcaption></figure><p></p>
<p>之后就可以遍历哈希表，因为对同一个页面进行修改的<code>redo</code>日志都放在了一个槽里，所以可以一次性将一个页面修复好（避免了很多读取页面的随机IO），这样可以加快恢复速度。另外需要注意一点的是，同一个页面的<code>redo</code>日志是按照生成时间顺序进行排序的，所以恢复的时候也是按照这个顺序进行恢复，如果不按照生成时间顺序进行排序的话，那么可能出现错误。比如原先的修改操作是先插入一条记录，再删除该条记录，如果恢复时不按照这个顺序来，就可能变成先删除一条记录，再插入一条记录，这显然是错误的。</p>
</li>
<li>
<p>跳过已经刷新到磁盘的页面</p>
<p>我们前边说过，<code>checkpoint_lsn</code>之前的<code>redo</code>日志对应的脏页确定都已经刷到磁盘了，但是<code>checkpoint_lsn</code>之后的<code>redo</code>日志我们不能确定是否已经刷到磁盘，主要是因为在最近做的一次<code>checkpoint</code>后，可能后台线程又不断的从<code>LRU链表</code>和<code>flush链表</code>中将一些脏页刷出<code>Buffer Pool</code>。这些在<code>checkpoint_lsn</code>之后的<code>redo</code>日志，如果它们对应的脏页在奔溃发生时已经刷新到磁盘，那在恢复时也就没有必要根据<code>redo</code>日志的内容修改该页面了。</p>
<p>那在恢复时怎么知道某个<code>redo</code>日志对应的脏页是否在奔溃发生时已经刷新到磁盘了呢？这还得从页面的结构说起，我们前边说过每个页面都有一个称之为<code>File Header</code>的部分，在<code>File Header</code>里有一个称之为<code>FIL_PAGE_LSN</code>的属性，该属性记载了最近一次修改页面时对应的<code>lsn</code>值（其实就是页面控制块中的<code>newest_modification</code>值）。如果在做了某次<code>checkpoint</code>之后有脏页被刷新到磁盘中，那么该页对应的<code>FIL_PAGE_LSN</code>代表的<code>lsn</code>值肯定大于<code>checkpoint_lsn</code>的值，凡是符合这种情况的页面就不需要重复执行lsn值小于<code>FIL_PAGE_LSN</code>的redo日志了，所以更进一步提升了奔溃恢复的速度。</p>
</li>
</ul>
<h2 class="heading">遗漏的问题：LOG_BLOCK_HDR_NO是如何计算的</h2>
<p>我们前边说过，对于实际存储<code>redo</code>日志的普通的<code>log block</code>来说，在<code>log block header</code>处有一个称之为<code>LOG_BLOCK_HDR_NO</code>的属性（忘记了的话回头再看看哈），我们说这个属性代表一个唯一的标号。这个属性是初次使用该block时分配的，跟当时的系统<code>lsn</code>值有关。使用下边的公式计算该block的<code>LOG_BLOCK_HDR_NO</code>值：</p>
<pre><code class="hljs bash" lang="bash">((lsn / 512) &amp; 0x3FFFFFFFUL) + 1
</code></pre><p>这个公式里的<code>0x3FFFFFFFUL</code>可能让大家有点困惑，其实它的二进制表示可能更亲切一点：</p>
<p></p><figure><img alt="image_1d4rt3sm81pbe1tij3pm147op9c30.png-36.9kB" src="https://user-gold-cdn.xitu.io/2019/3/26/169b8990bc261ef2?w=950&amp;h=295&amp;f=png&amp;s=37758"><figcaption></figcaption></figure><p></p>
<p>从图中可以看出，<code>0x3FFFFFFFUL</code>对应的二进制数的前2位为0，后30位的值都为<code>1</code>。我们刚开始学计算机的时候就学过，一个二进制位与0做与运算（<code>&amp;</code>）的结果肯定是0，一个二进制位与1做与运算（<code>&amp;</code>）的结果就是原值。让一个数和<code>0x3FFFFFFFUL</code>做与运算的意思就是要将该值的前2个比特位的值置为0，这样该值就肯定小于或等于<code>0x3FFFFFFFUL</code>了。这也就说明了，不论lsn多大，<code>((lsn / 512) &amp; 0x3FFFFFFFUL)</code>的值肯定在<code>0</code>~<code>0x3FFFFFFFUL</code>之间，再加1的话肯定在<code>1</code>~<code>0x40000000UL</code>之间。而<code>0x40000000UL</code>这个值大家应该很熟悉，这个值就代表着<code>1GB</code>。也就是说系统最多能产生不重复的<code>LOG_BLOCK_HDR_NO</code>值只有<code>1GB</code>个。设计InnoDB的大叔规定<code>redo</code>日志文件组中包含的所有文件大小总和不得超过512GB，一个block大小是512字节，也就是说redo日志文件组中包含的block块最多为1GB个，所以有1GB个不重复的编号值也就够用了。</p>
<p>另外，<code>LOG_BLOCK_HDR_NO</code>值的第一个比特位比较特殊，称之为<code>flush bit</code>，如果该值为1，代表着本block是在某次将<code>log buffer</code>中的block刷新到磁盘的操作中的第一个被刷入的block。</p>
